﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Wisdom.Core;
using Wisdom.Core.Repositories;
using Wisdom.Core.Specifications;

namespace Wisdom.Repositories.EntityFramework
{
    /// <summary>
    /// Represents the Entity Framework repository.
    /// </summary>
    /// <typeparam name="TAggregateRoot">The type of the aggregate root.</typeparam>
    public class EntityFrameworkRepository<TAggregateRoot> : Repository<TAggregateRoot>
        where TAggregateRoot : class, IAggregateRoot
    {
        #region Private Fields
        private readonly IEntityFrameworkRepositoryContext efContext;
        #endregion

        #region Ctor
        /// <summary>
        /// Initializes a new instace of <c>EntityFrameworkRepository</c> class.
        /// </summary>
        /// <param name="context">The repository context.</param>
        public EntityFrameworkRepository(IRepositoryContext context)
            : base(context)
        {
            if (context is IEntityFrameworkRepositoryContext)
                this.efContext = context as IEntityFrameworkRepositoryContext;
        }
        #endregion

        #region Private Methods
        private MemberExpression GetMemberInfo(LambdaExpression lambda)
        {
            if (lambda == null)
                throw new ArgumentNullException("lambda");

            MemberExpression memberExpr = null;

            if (lambda.Body.NodeType == ExpressionType.Convert)
            {
                memberExpr =
                    ((UnaryExpression)lambda.Body).Operand as MemberExpression;
            }
            else if (lambda.Body.NodeType == ExpressionType.MemberAccess)
            {
                memberExpr = lambda.Body as MemberExpression;
            }

            if (memberExpr == null)
                throw new ArgumentException("method");

            return memberExpr;
        }

        private string GetEagerLoadingPath(Expression<Func<TAggregateRoot, dynamic>> eagerLoadingProperty)
        {
            MemberExpression memberExpression = this.GetMemberInfo(eagerLoadingProperty);
            var parameterName = eagerLoadingProperty.Parameters.First().Name;
            var memberExpressionStr = memberExpression.ToString();
            var path = memberExpressionStr.Replace(parameterName + ".", "");
            return path;
        }
        #endregion

        #region Overrides of Repository<TAggregateRoot>

        /// <summary>
        /// Adds an aggregate root to the repository.
        /// </summary>
        /// <param name="aggregateRoot">The aggregate root to be added to the repository.</param>
        protected override void DoAdd(TAggregateRoot aggregateRoot)
        {
            this.efContext.RegisterNew(aggregateRoot);
        }

        /// <summary>
        /// Removes the aggregate root from current repository.
        /// </summary>
        /// <param name="aggregateRoot">The aggregate root to be removed.</param>
        protected override void DoRemove(TAggregateRoot aggregateRoot)
        {
            this.efContext.RegisterDeleted(aggregateRoot);
        }

        /// <summary>
        /// Updates the aggregate root in the current repository.
        /// </summary>
        /// <param name="aggregateRoot">The aggregate root to be updated.</param>
        protected override void DoUpdate(TAggregateRoot aggregateRoot)
        {
            this.efContext.RegisterModified(aggregateRoot);
        }

        /// <summary>
        /// Checkes whether the aggregate root, which matches the given specification, exists in the repository.
        /// </summary>
        /// <param name="specification">The specification with which the aggregate root should match.</param>
        /// <returns>True if the aggregate root exists, otherwise false.</returns>
        protected override bool DoExists(ISpecification<TAggregateRoot> specification)
        {
            return this.efContext.Context.Set<TAggregateRoot>().Any(specification.IsSatisfiedBy);
        }

        /// <summary>
        /// Gets the aggregate root instance from repository by a given key.
        /// </summary>
        /// <param name="key">The key of the aggregate root.</param>
        /// <returns>The instance of the aggregate root.</returns>
        protected override TAggregateRoot DoFindByKey(Guid key)
        {
            return efContext.Context.Set<TAggregateRoot>().First(p => p.Id == key);
        }

        /// <summary>
        /// Finds a single aggregate root that matches the given specification.
        /// </summary>
        /// <param name="specification">The specification with which the aggregate root should match.</param>
        /// <param name="eagerLoadingProperties">The properties for the aggregated objects that need to be loaded.</param>
        /// <returns>The instance of the aggregate root.</returns>
        protected override TAggregateRoot DoFind(ISpecification<TAggregateRoot> specification, params Expression<Func<TAggregateRoot, dynamic>>[] eagerLoadingProperties)
        {
            var queryable = CreateEagerLoadingQueryable(specification, eagerLoadingProperties);
            return queryable.FirstOrDefault();
        }

        /// <summary>
        /// Gets all the aggregate roots that match the given specification, and sorts the aggregate roots
        /// by using the provided sort predicate and the specified sort order.
        /// </summary>
        /// <param name="specification">The specification with which the aggregate roots should match.</param>
        /// <param name="orderByParameters">The <see>
        ///                                       <cref>OrderyByParameter</cref>
        ///                                     </see> array which is used for sorting.</param>
        /// <param name="eagerLoadingProperties">The properties for the aggregated objects that need to be loaded.</param>
        /// <returns>All the aggregate roots that match the given specification and were sorted by using the given sort predicate and the sort order.</returns>
        protected override IEnumerable<TAggregateRoot> DoFindAll(ISpecification<TAggregateRoot> specification, OrderByParameter<TAggregateRoot>[] orderByParameters, params Expression<Func<TAggregateRoot, dynamic>>[] eagerLoadingProperties)
        {
            var queryable = CreateEagerLoadingQueryable(specification, eagerLoadingProperties);

            if (orderByParameters != null && orderByParameters.Length > 0)
            {
                queryable = CreateOrderByQueryable(orderByParameters, queryable);
            }
            return queryable.ToList();
        }

        /// <summary>
        /// Gets all the aggregate roots that match the given specification with paging enabled, and sorts the aggregate roots
        /// by using the provided sort predicate and the specified sort order.
        /// </summary>
        /// <param name="specification">The specification with which the aggregate roots should match.</param>
        /// <param name="pageNumber">The page number.</param>
        /// <param name="pageSize">The number of objects per page.</param>
        /// <param name="orderByParameters">The <see>
        ///                                       <cref>OrderyByParameter</cref>
        ///                                     </see> array which is used for sorting.</param>
        /// <param name="eagerLoadingProperties">The properties for the aggregated objects that need to be loaded.</param>
        /// <returns>All the aggregate roots that match the given specification and were sorted by using the given sort predicate and the sort order.</returns>
        protected override IEnumerable<TAggregateRoot> DoFindAll(ISpecification<TAggregateRoot> specification, int pageNumber, int pageSize, OrderByParameter<TAggregateRoot>[] orderByParameters, params Expression<Func<TAggregateRoot, dynamic>>[] eagerLoadingProperties)
        {
            if (pageNumber <= 0)
                throw new ArgumentOutOfRangeException("pageNumber", pageNumber, "The pageNumber is one-based and should be larger than zero.");
            if (pageSize <= 0)
                throw new ArgumentOutOfRangeException("pageSize", pageSize, "The pageSize is one-based and should be larger than zero.");

            int skip = (pageNumber - 1) * pageSize;
            int take = pageSize;

            var queryable = CreateEagerLoadingQueryable(specification, eagerLoadingProperties);

            if (orderByParameters != null && orderByParameters.Length > 0)
            {
                queryable = CreateOrderByQueryable(orderByParameters, queryable);
            }
            return queryable.Skip(skip).Take(take).ToList();
        }


        private IQueryable<TAggregateRoot> CreateOrderByQueryable(OrderByParameter<TAggregateRoot>[] orderByParameters, IQueryable<TAggregateRoot> queryable)
        {
            var orderedQueryable = queryable as IOrderedQueryable<TAggregateRoot>;
            if (orderedQueryable == null)
                return null;
            for (int i = 0; i < orderByParameters.Length; i++)
            {
                var orderByParameter = orderByParameters[i];
                if (i == 0)
                {
                    switch (orderByParameter.SortOrder)
                    {
                        case SortOrder.Ascending:
                            orderedQueryable = orderedQueryable.OrderBy(orderByParameter.KeySelector);
                            break;
                        case SortOrder.Descending:
                            orderedQueryable = orderedQueryable.OrderByDescending(orderByParameter.KeySelector);
                            break;
                    }
                }
                else
                {
                    switch (orderByParameter.SortOrder)
                    {
                        case SortOrder.Ascending:
                            orderedQueryable = orderedQueryable.ThenBy(orderByParameter.KeySelector);
                            break;
                        case SortOrder.Descending:
                            orderedQueryable = orderedQueryable.ThenByDescending(orderByParameter.KeySelector);
                            break;
                    }
                }
            }
            queryable = orderedQueryable.AsQueryable();
            return queryable;
        }

        private IQueryable<TAggregateRoot> CreateEagerLoadingQueryable(ISpecification<TAggregateRoot> specification, Expression<Func<TAggregateRoot, dynamic>>[] eagerLoadingProperties)
        {
            var dbset = efContext.Context.Set<TAggregateRoot>();
            IQueryable<TAggregateRoot> queryable;
            if (eagerLoadingProperties != null && eagerLoadingProperties.Length > 0)
            {
                var eagerLoadingProperty = eagerLoadingProperties[0];
                var eagerLoadingPath = this.GetEagerLoadingPath(eagerLoadingProperty);
                var dbquery = dbset.Include(eagerLoadingPath);
                for (int i = 1; i < eagerLoadingProperties.Length; i++)
                {
                    eagerLoadingProperty = eagerLoadingProperties[i];
                    eagerLoadingPath = this.GetEagerLoadingPath(eagerLoadingProperty);
                    dbquery = dbquery.Include(eagerLoadingPath);
                }
                queryable = dbquery.Where(specification.IsSatisfiedBy).AsQueryable();
            }
            else
                queryable = dbset.Where(specification.IsSatisfiedBy).AsQueryable();
            return queryable;
        }
        #endregion
    }
}
